/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package cz.atlascon.java2gpb.serialization;

import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors.EnumValueDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;
import com.google.protobuf.Message.Builder;
import org.apache.commons.lang.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Tom
 */
public class GPBSerializers {

    private static final Logger LOGGER = LoggerFactory.getLogger(GPBSerializers.class);
    private static final Set<Class> directlyAssignable = new HashSet<>(Arrays.asList(new Class[]{
            Byte.class, byte.class,
            Short.class, short.class,
            Integer.class, int.class,
            Long.class, long.class,
            Float.class, float.class,
            Double.class, double.class,
            Boolean.class, boolean.class,
            String.class,
            ByteString.class
    }));
    private final Map<Class, GPBSerializer> serializers;
    private final RepeatedSerializer repeated;
    private final EnumSerializer enumSerializer;
    private static final Map<String, GPBSerializers> instances = new ConcurrentHashMap<>();
    private static final ReentrantLock lock = new ReentrantLock();

    public static GPBSerializers getInstance(String instanceName) {
        lock.lock();
        try {
            GPBSerializers get = instances.get(instanceName);
            if (get == null) {
                get = new GPBSerializers();
                instances.put(instanceName, get);
            }
            return get;
        } finally {
            lock.unlock();
        }
    }

    private GPBSerializers() {
        serializers = new ConcurrentHashMap<>();
        repeated = new RepeatedSerializer();
        enumSerializer = new EnumSerializer();
        for (Class c : directlyAssignable) {
            serializers.put(c, new GPBSerializer() {
                @Override
                public void serialize(String name, Object obj, Builder builder) {
                    if (obj != null) {
                        setField(name, obj, builder);
                    } else {
//                        System.out.println("null val "+name);
                    }
                }

                @Override
                public Object deserialize(ClassInfo cls, Object gpbValue) throws Exception {
                    return gpbValue;
                }

                @Override
                public String toString() {
                    return "DirectSerializer";
                }
            });
        }
        serializers.put(byte[].class, new GPBSerializer() {
            @Override
            public void serialize(String name, Object obj, Builder builder) {
                if (obj != null) {
                    if (obj instanceof byte[]) {
                        setField(name, ByteString.copyFrom((byte[]) obj), builder);
                    } else {
                        throw new IllegalArgumentException("Invalid object type! " + obj.getClass());
                    }
                }
            }

            @Override
            public Object deserialize(ClassInfo cls, Object gpbValue) throws Exception {
                return ((ByteString) gpbValue).toByteArray();
            }

            @Override
            public String toString() {
                return "ByteArraySerializer";
            }
        });
    }

    public void addCustom(GPBSerializer<?> c) {
        ParameterizedType genericSuperclass = (ParameterizedType) c.getClass().getGenericSuperclass();
        Type[] args = genericSuperclass.getActualTypeArguments();
        Class inCls = (Class) args[0];
        serializers.put(inCls, c);
    }

    public static void setField(String name, Object obj, Builder builder) {
        if (obj != null) {
            try {
                FieldDescriptor fd = builder.getDescriptorForType().findFieldByName(name);
                if (fd.isRepeated()) {
                    builder.addRepeatedField(fd, obj);
                } else {
                    builder.setField(fd, obj);
                }
            } catch (Exception e) {
                LOGGER.error("Error setting field " + name + " for " + builder.getDescriptorForType().getFullName());
                throw e;
            }
        }
    }

    public Message serialize(Object o) throws Exception {
        SerializationInspector ins = SerializationInspector.get(o.getClass());
        Builder mainBuilder = ins.newBuilder();
        GPBObjectSerializer os = new GPBObjectSerializer(this);
        os.serialize(o, o.getClass(), mainBuilder);
        return mainBuilder.build();
    }

    public <E> E deserialize(Class<E> cls, Message msg) throws Exception {
        GPBObjectSerializer os = new GPBObjectSerializer(this);
        Object deser = os.deserialize(ClassInfo.get(cls, null), msg);
        return (E) deser;
    }

    public GPBSerializer getSerializer(GPBSerializable a, Class cls) {
        GPBSerializer ser = forClass(cls);
        if (ser != null) {
            return ser;
        } else if (a != null && a.serializer() != Void.class) {
            return instantiate(a.serializer());
        } else {
            return new GPBObjectSerializer(this);
        }
    }

    private <C extends Class> GPBSerializer forClass(C cls) {
        GPBSerializer ser = serializers.get(cls);
        if (ser == null) {
            if (isSupportedByRepeated(cls)) {
                return repeated;
            } else if (cls.isEnum()) {
                return enumSerializer;
            }
        }
        return ser;
    }

    public GPBSerializer instantiate(Class serializer) {
        if (hasInterface(serializer, GPBSerializer.class)) {
            try {
                Constructor[] constructors = serializer.getConstructors();
                for (Constructor c : constructors) {
                    if (c.getParameterTypes().length == 1 && c.getParameterTypes()[0] == GPBSerializers.class) {
                        return (GPBSerializer) c.newInstance(this);
                    }
                }
                return (GPBSerializer) serializer.newInstance();
            } catch (IllegalAccessException | InstantiationException | IllegalArgumentException | InvocationTargetException ex) {
                throw new RuntimeException("Unable to create instance of " + serializer + "!", ex);
            }
        } else {
            throw new IllegalArgumentException("Class " + serializer + " is not a GPB serializer!");
        }
    }

    public static boolean hasInterface(Class cls, Class iface) {
        Class[] interfaces = cls.getInterfaces();
        if (cls.isInterface()) {
            interfaces = (Class[]) ArrayUtils.add(interfaces, cls);
        }
        Class sc = cls;
        while ((sc = sc.getSuperclass()) != null) {
            interfaces = (Class[]) ArrayUtils.addAll(interfaces, sc.getInterfaces());
        }
        Set<Class> ifaces = getInterfaces(interfaces);
        return ifaces.contains(iface);
    }

    public static boolean isSupportedByRepeated(Class cls) {
        if (cls.isArray()) {
            return true;
        }
        return hasInterface(cls, Iterable.class);
    }

    private static Set<Class> getInterfaces(Class[] ifs) {
        Set<Class> browsed = new HashSet<>();
        Set<Class> found = new HashSet<>(Arrays.asList(ifs));
        while (!found.isEmpty()) {
            browsed.addAll(found);
            Set<Class> nfs = new HashSet<>();
            for (Class c : found) {
                nfs.addAll(Arrays.asList(c.getInterfaces()));
            }
            nfs.removeAll(browsed);
            found.clear();
            found.addAll(nfs);
        }
        return browsed;
    }

    private class RepeatedSerializer implements GPBSerializer {

        public RepeatedSerializer() {
        }

        @Override
        public String toString() {
            return "RepeatedSerializer";
        }

        private void checkAndAssign(Object next, String name, Builder builder) throws Exception {
            GPBSerializer serializer = getSerializer(null, next.getClass());
            serializer.serialize(name, next, builder);
            //setField(name, next, builder);
//            throw new IllegalArgumentException("Illegal serialized type " + next.getClass());
        }

        @Override
        public void serialize(String name, Object obj, Builder builder) throws Exception {
            if (obj instanceof Iterable) {
                // iterable
                Iterable i = (Iterable) obj;
                Iterator it = i.iterator();
                while (it.hasNext()) {
                    Object next = it.next();
                    checkAndAssign(next, name, builder);
                }

            } else if (obj.getClass().isArray()) {
                // array
                int size = Array.getLength(obj);
                for (int i = 0; i < size; i++) {
                    Object next = Array.get(obj, i);
                    checkAndAssign(next, name, builder);
                }
            } else {
                throw new IllegalArgumentException("Unsupported class : " + obj.getClass());
            }
        }

        @Override
        public Object deserialize(ClassInfo cls, Object gpbValue) throws Exception {
            Collection col = (Collection) gpbValue;
            List vals = new ArrayList();
            for (Object o : col) {
//                System.out.println("Deserializing "+o.getClass());
                GPBSerializer serializer = getSerializer(null, o.getClass());
                // parameter type now becomes main type
                Object deSer = serializer.deserialize(ClassInfo.get(cls.getParamClass(), null), o);
                vals.add(deSer);
            }
//            System.out.println("Done!");

            if (cls.getFieldClass().isArray()) {
                return vals.toArray();
            } else if (hasInterface(cls.getFieldClass(), Iterable.class)) {
                return vals;
            } else {
                throw new IllegalArgumentException("Unsupported class " + cls);
            }
        }
    }

    private class EnumSerializer implements GPBSerializer {

        public EnumSerializer() {
        }

        @Override
        public String toString() {
            return "EnumSerializer";
        }

        @Override
        public void serialize(String name, Object obj, Builder builder) throws Exception {
            if (obj == null) {
                // do nothing with null values
                return;
            }
            if (obj.getClass().isEnum()) {
                SerializationInspector ins = SerializationInspector.get(obj.getClass());
                GPBSerializable a = ins.getAnnotation();
                Method m = a.className().getMethod("getDescriptor", new Class[0]);
                Object enumDesc = m.invoke(null);
                Method m2 = enumDesc.getClass().getMethod("findValueByName", String.class);
                Object enumVal = m2.invoke(enumDesc, obj.toString());
                setField(name, enumVal, builder);
            } else {
                throw new IllegalArgumentException("Unsupported class : " + obj.getClass());
            }
        }

        @Override
        public Object deserialize(ClassInfo cls, Object gpbValue) throws Exception {
            if (!cls.getFieldClass().isEnum()) {
                throw new IllegalArgumentException(cls + " not an enum type!");
            }
            EnumValueDescriptor desc = (EnumValueDescriptor) gpbValue;
            Enum val = Enum.valueOf(cls.getFieldClass(), desc.getName());
            return val;
        }
    }
}
